﻿// Copyright (c)  YISH. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Text;
using NSQL.DateFormats;
using NSQL.Expressions;
using NSQL.Expressions.Functions;
using NSQL.Extensions;
using NSQL.Statements;

namespace NSQL.Generator
{
    public class NpgsqlGenerator : BaseGenerator, ISqlGenerator, IFunctionVisitor<StringBuilder>
    {
        public static SqlGeneratorSettings DefaultSettings => new SqlGeneratorSettings
        {
            LetterCase = LetterCase.LowerCase,
            DatetimeFormatter = NpgsqlDateFormatter.Instance
        };
        public static readonly ISqlGenerator Instance = new NpgsqlGenerator(DefaultSettings);

        private readonly NpgsqlKeyWords _;

        StringBuilder IFunctionVisitor<StringBuilder>.Visit(ICastFunction castFunction)
        {
            var targetType = castFunction.TargetType;
            if (string.Equals(targetType, "datetime", StringComparison.OrdinalIgnoreCase))
            {
                targetType = "timestamp";
            }
            var builder = new StringBuilder();
            builder.Append(castFunction.Expr.Accept(this));
            builder.Append("::");
            builder.Append(targetType);
            return builder;
        }

        StringBuilder IFunctionVisitor<StringBuilder>.Visit(ILogFunction expression)
        {
            var builder = new StringBuilder();
            builder.Append(_.Log);
            builder.Append('(');

            if (!(expression.Base is null))
            {
                builder.Append(expression.Base.Accept(this));

                builder.Append(", ");
            }

            builder.Append(expression.Number.Accept(this));
            builder.Append(')');
            
            return builder;
        }
        
        StringBuilder IFunctionVisitor<StringBuilder>.Visit(ITrimFunction trimFunction)
        {
            var builder = new StringBuilder();

            if (trimFunction.Type == TrimType.Both)
            {
                var hasTrimExpr = !(trimFunction.TrimExpr is null);
                if (hasTrimExpr)
                {
                    builder.Append(_.Trim);
                }
                else
                {
                    switch (trimFunction.Type)
                    {
                        case TrimType.Both:
                            builder.Append(_.Trim);
                            break;
                        case TrimType.Leading:
                            builder.Append(_.TrimStart);
                            break;
                        case TrimType.Trailing:
                            builder.Append(_.TrimEnd);
                            break;
                        default:
                            throw new ArgumentOutOfRangeException();
                    }
                }

                builder.Append('(');

                if (hasTrimExpr)
                {
                    switch (trimFunction.Type)
                    {
                        case TrimType.Both:
                            builder.Append(_.Both);
                            break;
                        case TrimType.Leading:
                            builder.Append(_.Leading);
                            break;
                        case TrimType.Trailing:
                            builder.Append(_.Trailing);
                            break;
                        default:
                            throw new ArgumentOutOfRangeException();
                    }


                    builder.Append(' ').Append(trimFunction.TrimExpr.Accept(this));
                    builder.Append(' ').Append(_.From).Append(' ');
                }

                builder.Append(trimFunction.Expr.Accept(this));
                builder.Append(')');
            }
            else
            {
                switch (trimFunction.Type)
                {
                    case TrimType.Both:
                        builder.Append(_.Trim);
                        break;
                    case TrimType.Leading:
                        builder.Append(_.TrimStart);
                        break;
                    case TrimType.Trailing:
                        builder.Append(_.TrimEnd);
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }

                builder.Append('(');
                builder.Append(trimFunction.Expr.Accept(this));
                if (trimFunction.TrimExpr != null)
                {
                    builder.Append(", ").Append(trimFunction.TrimExpr.Accept(this));
                }

                builder.Append(')');
            }

            return builder;
        }

        protected override StringBuilder FormatPaging(StringBuilder statement, int skip, int limit)
        {
            if (skip > 0)
            {
                statement.Append(' ').Append(_.Offset);
                statement.Append(' ').Append(skip);
            }

            if (limit <= 0) return statement;
            statement.Append(' ').Append(_.Limit);
            statement.Append(' ').Append(limit);
            return statement;
        }

        string ISqlGenerator.Generate(MinusStatement statement)
        {
            var builder = new StringBuilder();
            if (statement.Left.HasPaging())
            {
                
                builder.Append('(').Append(statement.Left.Accept(this)).Append(')');
            }
            else
            {
                builder.Append(statement.Left.Accept(this));
            }
            builder.Append(' ').Append(_.Except).Append(' ');
            if (statement.Right.HasPaging())
            {
                builder.Append('(').Append(statement.Right.Accept(this)).Append(')');
            }
            else
            {
                
                builder.Append(statement.Right.Accept(this));
            }
            return FormatPaging(builder, statement.Skip, statement.Limit).ToString();
        }

        StringBuilder IFunctionVisitor<StringBuilder>.Visit(IDiffFunction expression)
        {
            var builder = new StringBuilder();
            builder.Append(_.Extract).Append('(');
            builder.Append(_.Epoch).Append(' ').Append(_.From);
            builder.Append(' ').Append('(').Append(expression.End.Accept(this)).Append(" - ")
                .Append(expression.Start.Accept(this)).Append(')');
            builder.Append(')');
            switch (expression.Type)
            {
                case ExtractType.Year:
                    builder.Append(" / ").Append(60 * 60 * 24 * 365);
                    break;
                case ExtractType.Month:
                    builder.Append(" / ").Append(60 * 60 * 24 * 30);
                    break;
                case ExtractType.Week:
                    builder.Append(" / ").Append(60 * 60 * 24 * 7);
                    break;
                case ExtractType.Day:
                    builder.Append(" / ").Append(60 * 60 * 24);
                    break;
                case ExtractType.Hour:
                    builder.Append(" / ").Append(60 * 60);
                    break;
                case ExtractType.Minute:
                    builder.Append(" / ").Append(60);
                    break;
                case ExtractType.Second:
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }

            return builder;
        }

        StringBuilder IFunctionVisitor<StringBuilder>.Visit(INvlFunction nvlFunction)
        {
            var hasThen = !(nvlFunction.Then is null);

            if (hasThen)
            {
                return new MatchExpression(
                    new[]
                    {
                        new MatchPair<IPredicateExpression, IExpression>()
                        {
                            When = nvlFunction.Expr.IsNull(),
                            Then = nvlFunction.Default
                        }
                    },
                    nvlFunction.Then).Accept(this);
            }

            var builder = new StringBuilder();
            builder.Append(_.Coalesce);
            builder.Append('(');
            builder.Append(nvlFunction.Expr.Accept(this));
            builder.Append(", ").Append(nvlFunction.Default.Accept(this));
            builder.Append(')');
            return builder;
        }

        public NpgsqlGenerator(Func<SqlGeneratorSettings, SqlGeneratorSettings> setupAction)
            : this(setupAction(DefaultSettings))
        {
        }
        
        public NpgsqlGenerator(SqlGeneratorSettings settings) : this(settings, new NpgsqlKeyWords(settings.LetterCase))
        {
        }
        
        private NpgsqlGenerator(SqlGeneratorSettings settings, NpgsqlKeyWords keyWords) : base(settings, keyWords)
        {
            _ = keyWords;
        }
        
        protected class NpgsqlKeyWords : KeyWords
        {
            public NpgsqlKeyWords(LetterCase letterCase) : base(letterCase)
            {
            }
            
            public string Offset => FormatLetter(nameof(Offset));
            public string Except => FormatLetter(nameof(Except));
            public string Epoch => FormatLetter(nameof(Epoch));

            public override string PadLeft => FormatLetter("LPad");
            public override string PadRight => FormatLetter("RPad");
            public override string TrimStart => FormatLetter("LTrim");
            public override string TrimEnd => FormatLetter("RTrim");
        }
    }
}